package com.fanshuai.springcloud.route;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

@Service
@Slf4j
public class RouteService implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;

    private String dataId = "gateway-nacos-route";
    private String group = "gateway-nacos";

    @Value("${spring.cloud.nacos.config.server-addr}")
    private String serverAddr;

    @Resource
    private RouteDefinitionWriter routeDefinitionWriter;

    private ConfigService configService;

    private List<String> routeIds = new ArrayList<>();

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    @PostConstruct
    public void init() {
        try {
            configService = NacosFactory.createConfigService(serverAddr);
            String json = configService.getConfig(dataId, group, 5000);
            refreshRoute(json);

            configService.addListener(dataId, group, new Listener() {
                @Override
                public Executor getExecutor() {
                    return null;
                }

                @Override
                public void receiveConfigInfo(String configInfo) {
                    refreshRoute(json);
                }
            });
        } catch (Exception e) {
            log.error("" + e);
        }
    }

    private void refreshRoute(String json) {
        try {
            List<RouteDefinition> definitions = getRouteInfo(json);
            if (CollectionUtils.isEmpty(definitions)) {
                return;
            }

            clearRoute();
            for (RouteDefinition definition : definitions) {
                addRoute(definition);
            }
            fireRefreshEvent();
        } catch (Exception e) {
            log.error("" + e);
        }
    }

    private void clearRoute() {
        for (String id : routeIds) {
            routeDefinitionWriter.delete(Mono.just(id)).subscribe();
        }
        routeIds.clear();
    }
    private void addRoute(RouteDefinition routeDefinition) {
        try {
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        } catch (Exception e) {
            log.error("" + e);
        }
    }

    private void fireRefreshEvent() {
        publisher.publishEvent(new RefreshRoutesEvent(routeDefinitionWriter));
    }

    private List<RouteDefinition> getRouteInfo(String json) throws Exception {
        List<RouteDefinition> definitions = new ArrayList<>();

        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        RouteConfigBean routeConfigBean = mapper.readValue(json, RouteConfigBean.class);

        for (String key : routeConfigBean.keySet()) {
            RouteInfo routeInfo = routeConfigBean.get(key);
            definitions.add(getDefinition(key, routeInfo));
        }
        return definitions;
    }

    private RouteDefinition getDefinition(String key, RouteInfo routeInfo) {
        RouteDefinition definition = new RouteDefinition();
        definition.setId(key);
        try {
            definition.setUri(new URI(routeInfo.getUri()));
        } catch (Exception e) {
            log.error("" + e);
        }


        List<PredicateDefinition> predicateDefinitions = new ArrayList<>();
        if (!CollectionUtils.isEmpty(routeInfo.getHosts())) {
            for (String host: routeInfo.getHosts()) {
                String s = String.format("Host=%s", host);
                PredicateDefinition predicateDefinition = new PredicateDefinition(s);

                predicateDefinitions.add(predicateDefinition);
            }
            for (String path: routeInfo.getPaths()) {
                String s = String.format("Path=%s", path);
                PredicateDefinition predicateDefinition = new PredicateDefinition(s);

                predicateDefinitions.add(predicateDefinition);
            }
        }
        definition.setPredicates(predicateDefinitions);

        List<FilterDefinition> filterDefinitions = new ArrayList<>();
        if (!CollectionUtils.isEmpty(routeInfo.getFilters())) {
            for (String filter: routeInfo.getFilters()) {
                FilterDefinition filterDefinition = new FilterDefinition(filter);

                filterDefinitions.add(filterDefinition);
            }
        }
        definition.setFilters(filterDefinitions);

        return definition;
    }
}
